package com.heima.article.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.heima.article.mapper.ArticleConfigMapper;
import com.heima.article.mapper.ArticleContentMapper;
import com.heima.article.mapper.ArticleMapper;
import com.heima.article.pojo.ContentData;
import com.heima.article.service.ArticleService;
import com.heima.common.constants.ArticleConstants;
import com.heima.common.constants.RedisConstants;
import com.heima.common.exception.CustomException;
import com.heima.file.service.FileStorageService;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.article.dtos.ArticleHomeDto;
import com.heima.model.article.dtos.ArticleInfoDto;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.article.pojos.ApArticleConfig;
import com.heima.model.article.pojos.ApArticleContent;
import com.heima.model.article.vos.ArticleBehaviorVO;
import com.heima.model.article.vos.HotArticleVo;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.mess.ArticleVisitStreamMess;
import com.heima.model.search.vos.SearchArticleVo;
import com.heima.model.user.pojos.ApUser;
import com.heima.utils.thread.AppThreadLocalUtil;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.io.ByteArrayInputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

import static com.heima.common.constants.ArticleConstants.DEFAULT_TAG;
import static com.heima.common.constants.RedisConstants.HOT_ARTICLE_KEY;

/**
 * @author brianxia
 * @version 1.0
 * @date 2022-11-10 11:38
 */
@Service
@Slf4j
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, ApArticle> implements ArticleService {

    @Autowired
    private ArticleMapper articleMapper;
    @Autowired
    private ArticleConfigMapper articleConfigMapper;
    @Autowired
    private ArticleContentMapper articleContentMapper;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired //自动配置的原理
    private Configuration configuration;
    @Autowired
    private FileStorageService fileStorageService;
    @Autowired
    private KafkaTemplate<String,String> kafkaTemplate;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public List<ApArticle> load(ArticleHomeDto dto, Short type) {

        //0.判断如果type == null 就是加载首页， 优先从redis中获取
        if(type == null){
            String s = stringRedisTemplate.opsForValue().get(HOT_ARTICLE_KEY + dto.getTag());
            if(StringUtils.isNotBlank(s)){
                return JSON.parseArray(s, ApArticle.class);
            }
        }


        //1.校验参数
        Integer size = dto.getSize();
        if(size == null || size <= 0 || size >= 1000 ){
            size = 10;
        }

        //频道id  1 2 3 可能传null 默认加载推荐页 __all__ (和前端约定字符串)
        String tag = dto.getTag();
        if(StringUtils.isBlank(tag)){
            tag = DEFAULT_TAG;
        }

        //使用框架帮我们处理分页
        Page<ApArticle> page = new Page<>(1, size);
        page.setSearchCount(false);

        return articleMapper.load(
                page
                ,
                size,tag,dto.getMaxBehotTime(),dto.getMinBehotTime(),type);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long saveArticle(ArticleDto dto) {

        if(dto.getId() == null){
            //新增
//            ApArticle apArticle = new ApArticle();
//            BeanUtils.copyProperties(dto,apArticle);
            save(dto); //主表
            //配置表
            ApArticleConfig apArticleConfig = new ApArticleConfig(dto.getId());
            articleConfigMapper.insert(apArticleConfig);
            //内容表
            ApArticleContent apArticleContent = new ApArticleContent();
            apArticleContent.setContent(dto.getContent());
            apArticleContent.setArticleId(dto.getId());
            articleContentMapper.insert(apArticleContent);

        }else{
            //修改
            updateById(dto);
            //内容表
            articleContentMapper.update(null, Wrappers.<ApArticleContent>lambdaUpdate()
                    .eq(ApArticleContent::getArticleId,dto.getId())
                    .set(ApArticleContent::getContent,dto.getContent())
            );
        }


        //处理完了数据库中的数据，生成静态页面
        generateStaticHtml(dto.getContent(),dto);


        return dto.getId();
    }

    @Override
    public ArticleBehaviorVO loadArticleBehavior(ArticleInfoDto dto) {
        ArticleBehaviorVO vo = new ArticleBehaviorVO();

        ApUser user = AppThreadLocalUtil.getUser();
        if(user == null){
            //没登录 是游客返回默认值false
            return vo;
        }

        //获取点赞
        Boolean member = stringRedisTemplate.opsForSet().
                isMember(RedisConstants.LIKES_KEY + dto.getArticleId().toString(),
                        user.getId().toString());
        if(member != null && member){
            vo.setIslike(true);
        }

        //获取关注
        member = stringRedisTemplate.opsForSet().
                isMember(RedisConstants.FOLLOW_AUTHOR_KEY + dto.getAuthorId().toString(),
                        user.getId().toString());
        if(member != null && member){
            vo.setIsfollow(true);
        }
        return vo;
    }

    @Override
    public void hotArticle() {
        //1.查询最近5天的文章,涉及到多表关联查询

        //1.8之后使用jdk localdatetime
        LocalDateTime localDateTime = LocalDateTime.now().minusDays(5);
        //使用jdk Calendar
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DAY_OF_MONTH,-5);
        Date time = instance.getTime();

        //使用joda-time包 jdk1.8之前很常用
        Date date = DateTime.now().minusDays(5).toDate();
        List<ApArticle> apArticles = articleMapper.selectArticlesByDate(date);
        if(CollectionUtils.isEmpty(apArticles)){
            return;
        }
        //2.计算文章分值
        List<HotArticleVo> list = countArticleScore(apArticles);
        //3.缓存到redis
        cacheArticleToRedis(list);
    }

    @Override
    public void updateArticleBehavior(ArticleVisitStreamMess mess) {
        //如果4个值都是0，不需要执行后边代码
        if(mess.getView() == 0 && mess.getCollect() == 0 && mess.getLike() ==0 && mess.getComment() == 0 ){
            return;
        }

        //1.写入数据库
        articleMapper.updateBehavior(mess.getArticleId(),mess.getView(),mess.getComment(),mess.getCollect(),mess.getLike());

        //2.更新redis中文章分值
        ApArticle apArticle = getById(mess.getArticleId());
        HotArticleVo hotArticleVo = new HotArticleVo();
        BeanUtils.copyProperties(apArticle,hotArticleVo);
        //计算分值
        int count = countScore(apArticle);

        //更新数据到redis中  1.当前这篇文章所在频道 2.在推荐中

        hotArticleVo.setScore(count);

        //1.从redis中获取当前频道数据
        Integer channelId = apArticle.getChannelId();
        //更新当前频道下数据
        updateHotArticleInRedis(hotArticleVo, String.valueOf(channelId));
        //更新推荐
        updateHotArticleInRedis(hotArticleVo, DEFAULT_TAG);
    }

    private void updateHotArticleInRedis(HotArticleVo hotArticleVo, String channelId) {
        String cache = stringRedisTemplate.opsForValue().get(HOT_ARTICLE_KEY + channelId);
        if(StringUtils.isNotBlank(cache)){
            //缓存中有数据
            List<HotArticleVo> articleVos = JSON.parseArray(cache, HotArticleVo.class);
            boolean find = false;
            for (HotArticleVo articleVo : articleVos) {
                if(articleVo.getId().equals(hotArticleVo.getId())){
                    //找到了文章 更新分值
                    articleVo.setScore(hotArticleVo.getScore());
                    find = true;
                    break;
                }
            }
            //没找到
            if(!find){
                articleVos.add(hotArticleVo);
            }

            //排序 取前30条
            List<HotArticleVo> list = articleVos.stream().sorted(Comparator.comparing(HotArticleVo::getScore).reversed())
                    .limit(30).collect(Collectors.toList());
            //放入redis
            String s = JSON.toJSONString(list);
            //判断处理完的数据和一开始数据是否一样，如果不一样才保存
            if(!cache.equals(s)){
                stringRedisTemplate.opsForValue()
                        .set(HOT_ARTICLE_KEY + channelId,s);
            }

        }
    }

    private void cacheArticleToRedis(List<HotArticleVo> list) {
        //1.缓存每个频道下对应的数据

        //1.1先把推荐文章所有所在频道筛选出来
        List<Integer> channelIds = list.stream().map(HotArticleVo::getChannelId)
                .distinct().collect(Collectors.toList());
        //1.2筛选每个频道下文章数据
        for (Integer channelId : channelIds) {
            //提取这个频道下的文章 按分值倒序 取前30条
            List<HotArticleVo> articles = list.stream().filter(x -> channelId.equals(x.getChannelId()))
                    .sorted(Comparator.comparing(HotArticleVo::getScore).reversed())
                    .limit(30).collect(Collectors.toList());

            stringRedisTemplate.opsForValue().set(HOT_ARTICLE_KEY + channelId
                    ,JSON.toJSONString(articles) );
        }

        //2.处理 推荐 下数据

        List<HotArticleVo> articles = list.stream()
                .sorted(Comparator.comparing(HotArticleVo::getScore).reversed())
                .limit(30).collect(Collectors.toList());

        stringRedisTemplate.opsForValue().set(HOT_ARTICLE_KEY + DEFAULT_TAG
                ,JSON.toJSONString(articles) );

    }

    private List<HotArticleVo> countArticleScore(List<ApArticle> apArticles) {
        List<HotArticleVo> hotArticleVos = new ArrayList<>();
        for (ApArticle apArticle : apArticles) {
            HotArticleVo hotArticleVo = new HotArticleVo();
            BeanUtils.copyProperties(apArticle,hotArticleVo);
            //计算分值
            int count = countScore(apArticle);
            hotArticleVo.setScore(count);
            hotArticleVos.add(hotArticleVo);
        }
        return hotArticleVos;
    }

    private int countScore(ApArticle apArticle) {
        int score = 0;

        //阅读
        Integer views = apArticle.getViews();
        if(views != null && views > 0){
            score = score + (views * ArticleConstants.HOT_ARTICLE_READ_WEIGHT);
        }
        //点赞
        Integer likes = apArticle.getLikes();
        if(likes != null && likes > 0){
            score = score + (likes * ArticleConstants.HOT_ARTICLE_LIKE_WEIGHT);
        }

        Integer comment = apArticle.getComment();
        if(comment != null && comment > 0){
            score = score + (comment * ArticleConstants.HOT_ARTICLE_COMMENT_WEIGHT);
        }

        Integer collection = apArticle.getCollection();
        if(collection != null && collection > 0){
            score = score + (collection * ArticleConstants.HOT_ARTICLE_COLLECTION_WEIGHT);
        }
        return score;
    }

    @Async
    public void generateStaticHtml(String content,ApArticle apArticle) {

        try{
            List<ContentData> lists = objectMapper.readValue(content,
                    objectMapper.getTypeFactory().constructParametricType(List.class, ContentData.class));

            //freemarker的模板对象，获取模板
            Template template = configuration.getTemplate("article.ftl");
            Map model = new HashMap();
            model.put("content",lists);

            //先将文件输出到内存中
            StringWriter stringWriter = new StringWriter(); //字符串输出流，内存中存放字符串
            template.process(model, stringWriter);

            //转换成输入流，提交到minio
            ByteArrayInputStream inputStream = new ByteArrayInputStream(stringWriter.toString()
                    .getBytes(StandardCharsets.UTF_8));

            //文章详情路径
            String path = fileStorageService.uploadHtmlFile(null, apArticle.getId() + ".html"
                    , inputStream);

            //将数据更新到数据库
            articleMapper.update(null,Wrappers.<ApArticle>lambdaUpdate()
                    .eq(ApArticle::getId,apArticle.getId())
                    .set(ApArticle::getStaticUrl,path)
            );

            //同步数据到es
            SearchArticleVo searchArticleVo = new SearchArticleVo();
            BeanUtils.copyProperties(apArticle,searchArticleVo);
            searchArticleVo.setContent(content);
            searchArticleVo.setStaticUrl(path);
            kafkaTemplate.send(ArticleConstants.ARTICLE_ES_SYNC_TOPIC,
                    JSON.toJSONString(searchArticleVo));

        }catch (Exception e){
            log.error("生成静态页面失败",e);
        }

    }
}
